package pl.com.like.sequoia.maintenance.compare;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import org.biojava.bio.structure.Structure;
import org.biojava.bio.structure.StructureException;
import org.biojava.bio.structure.align.ClusterAltAligs;
import org.biojava.bio.structure.align.StructurePairAligner;
import org.biojava.bio.structure.align.pairwise.AlternativeAlignment;
import org.biojava.bio.structure.io.PDBFileReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import pl.com.like.sequoia.biojava.util.IDirHelper;
import pl.com.like.sequoia.dao.bio.IBioObjectAlignDao;
import pl.com.like.sequoia.dao.bio.IBioObjectHandlerDao;
import pl.com.like.sequoia.exceptions.LogicalException;
import pl.com.like.sequoia.model.bio.BioObjectAlign;
import pl.com.like.sequoia.model.bio.BioObjectHandler;
import pl.com.like.sequoia.model.bio.enums.FileTypeEnum;
import pl.com.sequoia.jms.PdbCompareMessage;
import pl.com.sequoia.jms.helper.IJmsPdbCompareHandler;

@Component
public class PdbComparatorService implements IJmsPdbCompareHandler {

	private final PDBFileReader pdbReader = new PDBFileReader();
	private final StructurePairAligner sc = new StructurePairAligner();
	private final static Logger log = LoggerFactory.getLogger(PdbComparatorService.class);

	@Autowired
	private IBioObjectHandlerDao bioObjectHandlerDao;
	@Autowired
	private IDirHelper dirHelper;
	@Autowired
	private IBioObjectAlignDao bioObjectAlignDao;

	// @Autowired
	// private ISearchParametersDao searchParametersDao;

	public File combineDestinationFile(String parent, String compareTo) {
		Assert.hasLength(parent);
		Assert.hasLength(compareTo);
		String name = parent + "_" + compareTo;
		return dirHelper.createDestFile(name, FileTypeEnum.PDB_ZIP);
	}

	public void compare(PdbCompareMessage message) {
		Assert.notNull(message);
		String parentName = message.getParentObject();
		List<String> compareList = message.getCompareToObjects();
		BioObjectHandler parentProt = getBioObject(parentName);
		Structure parentStruc = getStructureForElement(parentName);
		Boolean throwedExc = Boolean.FALSE;
		List<String> excMsgList = new ArrayList<String>();
		for (String compareElementId : compareList) {
			Structure compareStruc = getStructureForElement(compareElementId);
			BioObjectHandler compareProt = getBioObject(compareElementId);
			try {
				// do not compare, when comparison already exists
				if (!comparisonExists(parentName, compareElementId)) {
					File outFile = combineDestinationFile(parentName, compareElementId);
					sc.align(parentStruc, compareStruc);
					AlternativeAlignment[] aligs = sc.getAlignments();
					// cluster similar results together
					ClusterAltAligs.cluster(aligs);

					// print the result:
					// the AlternativeAlignment object gives access to rotation matrices
					// / shift vectors.
					String msg = MessageFormat.format(
							"comparation of {0} and {1} has finished. alternative alignment for comparisons are:",
							parentName, compareElementId);
					log.info(msg);
					AlternativeAlignment topAlign = null;
					for (int i = 0; i < aligs.length; i++) {
						AlternativeAlignment aa = aligs[i];
						// assign first element to top reference
						if (i == 0) {
							topAlign = aa;
						} else {
							// find align with the highest score
							if (topAlign.getScore() < aa.getScore()) {
								// assign top align to align with highest score
								topAlign = aa;
							}
						}
						log.info(aa.toString());
					}

					// convert AlternativeAlignment 1 to PDB file, so it can be opened
					// with a viewer of your choice
					// (e.g. Jmol, Rasmol)

					if (topAlign != null) {
						AlternativeAlignment aa1 = topAlign;
						String pdbstr = aa1.toPDB(parentStruc, compareStruc);

						log.info("writing alignment to " + outFile.getAbsolutePath());
						FileOutputStream out = new FileOutputStream(outFile);
						PrintStream p = new PrintStream(out);

						p.println(pdbstr);

						p.close();
						out.close();
						// at least 1 alignment exist comparing those two elements, so it is worth to save results.

						String cA = StringUtils.arrayToDelimitedString(topAlign.getPDBresnum1(), ",");
						String cB = StringUtils.arrayToDelimitedString(topAlign.getPDBresnum2(), ",");
						Float score = topAlign.getScore();
						BioObjectAlign alignResult = new BioObjectAlign();
						alignResult.setBioObjectA(parentProt);
						alignResult.setBioObjectB(compareProt);
						alignResult.setPath(outFile.getAbsolutePath());
						alignResult.setCreated(Calendar.getInstance().getTime());
						alignResult.setName(outFile.getName());
						alignResult.setSuccess(Boolean.TRUE);
						alignResult.setBioAAlign(cA);
						alignResult.setBioBAlign(cB);
						alignResult.setScore(score);
						alignResult.setClusters(topAlign.getCluster());
						alignResult.setGaps(topAlign.getGaps());
						alignResult.setPercId(topAlign.getPercId());
						bioObjectAlignDao.save(alignResult);
						log.info(MessageFormat.format("successfully saved comparison result between {0} vs. {1}",
								parentName, compareElementId));
					}
				} else {
					log.info(MessageFormat.format(
							"comparison result for {0} vs. {1} is already saved in database. is skipped.", parentName,
							compareElementId));
				}
			} catch (StructureException e) {
				String msg = MessageFormat.format("could not compute similarity of objects {0} vs. {1}", parentName,
						compareElementId);
				log.error(msg, e);
				File outFile = combineDestinationFile(parentName, compareElementId);
				throwedExc = Boolean.TRUE;
				excMsgList.add(msg);
				BioObjectAlign alignResult = new BioObjectAlign();
				alignResult.setBioObjectA(parentProt);
				alignResult.setBioObjectB(compareProt);
				alignResult.setPath(null);
				alignResult.setCreated(Calendar.getInstance().getTime());
				alignResult.setName(outFile.getName());
				alignResult.setSuccess(Boolean.FALSE);
				alignResult.setCompareDescription(msg + e.getMessage());
				bioObjectAlignDao.save(alignResult);
			} catch (IOException e) {
				throwedExc = Boolean.TRUE;
				excMsgList.add(e.getMessage());
			}
		}
		if (Boolean.TRUE.equals(throwedExc)) {
			throw new LogicalException(StringUtils.collectionToDelimitedString(excMsgList, "; "));
		}
	}

	public Boolean comparisonExists(String parentId, String childId) {
		return this.bioObjectAlignDao.comparisonExists(parentId, childId);
	}

	public BioObjectHandler getBioObject(String objectId) {
		Assert.notNull(objectId);
		return bioObjectHandlerDao.get(objectId, FileTypeEnum.PDB_ZIP);
	}

	public IBioObjectAlignDao getBioObjectAlignDao() {
		return bioObjectAlignDao;
	}

	public IBioObjectHandlerDao getBioObjectHandlerDao() {
		return bioObjectHandlerDao;
	}

	public IDirHelper getDirHelper() {
		return dirHelper;
	}

	/**
	 * Get structure for searched object
	 * 
	 * @param objectId id of searched object
	 * @throws LogicalException when could not find object in database or on filesystem.
	 * @return
	 */
	public Structure getStructureForElement(String objectId) throws LogicalException {
		BioObjectHandler element = getBioObject(objectId);
		Structure struc = null;
		if (element != null) {
			try {
				struc = pdbReader.getStructure(new File(element.getPath()));
			} catch (IOException e) {
				String msg = MessageFormat.format("could not read file from filesystem {0}", element.getPath());
				log.error(msg, e);
				throw new LogicalException(msg, e);
			}
		} else {
			String msg = MessageFormat.format(
					"could not find given element in local database. Please upload file with id {0}", objectId);
			log.error(msg);
			throw new LogicalException(msg);
		}
		return struc;
	}

	public void setBioObjectAlignDao(IBioObjectAlignDao bioObjectAlignDao) {
		this.bioObjectAlignDao = bioObjectAlignDao;
	}

	public void setBioObjectHandlerDao(IBioObjectHandlerDao bioObjectHandlerDao) {
		this.bioObjectHandlerDao = bioObjectHandlerDao;
	}

	public void setDirHelper(IDirHelper dirHelper) {
		this.dirHelper = dirHelper;
	}

}
